﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

namespace Concat
{
    class Program
    {
        private static Interpreter interpreter;
        private static Parser parser;
        private static string[] names = new string[] {
            "prompt", "error", 
            "stacktrace-name", "stacktrace-file", 
            "stacktrace-line", "stacktrace-col"
        };
        private static ConsoleColor[] colors = new ConsoleColor[] {
            ConsoleColor.Green, ConsoleColor.Red, 
            ConsoleColor.Yellow, ConsoleColor.Cyan, 
            ConsoleColor.Magenta, ConsoleColor.White
        };
        const int PROMPT = 0, ERROR = 1,
                  STACK_TRACE_NAME = 2, STACK_TRACE_FILE = 3,
                  STACK_TRACE_LINE = 4, STACK_TRACE_COL = 5;

        static void Main(string[] args)
        {
            Environment.CurrentDirectory = @"C:\Users\Grant\Desktop\Concat\Concat\bin\Debug\";

            if (args.Length > 2)
            {
                Console.WriteLine(@"usage: 
    concat              - runs Concat REPL
    concat *.cct        - runs the specified Concat file
    concat *.cct -repl  - runs the specified Concat file, then the REPL");
            }

            string file = null;
            bool hasFile = false;
            bool repl = true;
            if (args.Length != 0)
            {
                hasFile = true;
                file = args[0];
                repl = args.Length == 2;
            }

            interpreter = new Interpreter(10000);
            Builtins builtins = new Builtins(interpreter);
            parser = new Parser();
            builtins.setParser(parser);
            interpreter.defineAll(builtins);
            interpreter.define("include", new BuiltinWord(new string[] { "file" }, arg =>
            {
                string concatFile = arg[0].ToString();
                try
                {
                    interpreter.execFile(concatFile);
                    return new object[1] { true };
                }
                catch
                {
                    return new object[1] { false };
                }
            }));
            interpreter.execFile("../../system.cct");

            #region color stuff
            if (repl)
            {
                for (int i = 0; i < 6; i++)
                {
                    int j = i; // value, not variable (for closure)
                    interpreter.define("set-" + names[i] + "-color", 
                        new BuiltinWord(new string[] { "color" }, arg =>
                        {
                            colors[j] = (ConsoleColor)arg[0];
                            return new object[0];
                        }));
                }
                interpreter.define("make-color", new BuiltinWord(new string[] { "name" }, arg =>
                {
                    return new object[1] { Enum.Parse(typeof(ConsoleColor), ((Symbol)arg[0]).sym) };
                }));
            }
            #endregion
            #region other builtins
            if (repl)
            {
                interpreter.define("reset", new BuiltinWord(new string[0], _ =>
                {
                    Console.Clear();
                    Main(args);
                    Environment.Exit(0);
                    return new object[0];
                }));
                interpreter.define("clear-data", new BuiltinWord(new string[0], _ =>
                {
                    interpreter.clearDataStack();
                    return new object[0];
                }));
            }

            interpreter.define("exit", new BuiltinWord(new string[0], _ =>
            {
                Environment.Exit(0);
                //exit = true;
                return new object[0];
            }));
            #endregion

            if (hasFile) runFile(file);
            if (repl) doRepl();
        }
        static void doRepl()
        {
            int numInput = 1;
            List<string> lines = new List<string>();
            while (true)
            {
                int curline = numInput - 1;

                write(colors[PROMPT], numInput.ToString("000") + ">> ");
                string input = Console.ReadLine();
                numInput++;
                while (input.EndsWith("\\"))
                {
                    input = input.Remove(input.Length - 1) + "\n";
                    write(colors[PROMPT], numInput.ToString("000") + ".. ");
                    input += Console.ReadLine();
                    numInput++;
                }
                lines.AddRange(input.Split('\n'));

                execString(curline, input, lines);
                //Scanner scanner = new Scanner("repl", new StringReader(input), curline);
                //parser.setScanner(scanner);

                //Term[] prog = null;
                //try { prog = parser.parseAll(); }
                //catch (ScanException e) { writeln(colors[ERROR], e.Message); }
                //catch (ParseException e) { writeln(colors[ERROR], e.Message); }

                //if (prog != null) try
                //    {
                //        interpreter.execProg(prog);
                //    }
                //    catch (RuntimeException e)
                //    {
                //        Console.WriteLine();
                //        showErrorLoc(lines, e.stack.getLoc());
                //        writeln(colors[ERROR], e.Message);
                //        writeCallStack(colors[STACK_TRACE_NAME], colors[STACK_TRACE_FILE], colors[STACK_TRACE_LINE],
                //            colors[STACK_TRACE_COL], e.stack);
                //        interpreter.clearCallStack();
                //    }
                //    catch (Exception e)
                //    {
                //        Console.WriteLine();
                //        showErrorLoc(lines, interpreter.getCallStack().getLoc());
                //        writeln(colors[ERROR], e.Message);
                //        writeCallStack(colors[STACK_TRACE_NAME], colors[STACK_TRACE_FILE], colors[STACK_TRACE_LINE],
                //            colors[STACK_TRACE_COL], interpreter.getCallStack());
                //        interpreter.clearCallStack();
                //    }
            }
        }
        static void runFile(string file)
        {
            if (!File.Exists(file))
            {
                Console.Error.WriteLine("Could not find file '" + file + "' for interpretation.");
                return;
            }
            string text = File.ReadAllText(file);
            execString(0, text, text.Split('\n').ToList());
            //List<string> lines = new List<string>(File.ReadAllLines(file));
        }
        static void execString(int line, string input, List<string> lines)
        {
            Scanner scanner = new Scanner("repl", new StringReader(input), line);
            parser.setScanner(scanner);

            Term[] prog = null;
            try { prog = parser.parseAll(); }
            catch (ScanException e) { writeln(colors[ERROR], e.Message); }
            catch (ParseException e) { writeln(colors[ERROR], e.Message); }

            if (prog != null) try
                {
                    interpreter.execProg(prog);
                }
                catch (RuntimeException e)
                {
                    Console.WriteLine();
                    showErrorLoc(lines, e.stack.getLoc());
                    writeln(colors[ERROR], e.Message);
                    writeCallStack(colors[STACK_TRACE_NAME], colors[STACK_TRACE_FILE], colors[STACK_TRACE_LINE],
                        colors[STACK_TRACE_COL], e.stack);
                    interpreter.clearCallStack();
                }
                catch (Exception e)
                {
                    Console.WriteLine();
                    showErrorLoc(lines, interpreter.getCallStack().getLoc());
                    writeln(colors[ERROR], e.Message);
                    writeCallStack(colors[STACK_TRACE_NAME], colors[STACK_TRACE_FILE], colors[STACK_TRACE_LINE],
                        colors[STACK_TRACE_COL], interpreter.getCallStack());
                    interpreter.clearCallStack();
                }
        }

        static void showErrorLoc(List<string> lines, Location loc)
        {
            if (!loc.hasValues) return;

            string line = "";
            if (loc.file != "repl")
                line = File.ReadAllLines(loc.file)[loc.line];
            else line = lines[loc.line];
            Console.WriteLine("      " + line);
            Console.Write("      ");
            for (int i = 0; i < loc.col; i++)
                Console.Write(line[i] == '\t' ? '\t' : ' ');
            Console.WriteLine("^");
        }

        static void writeCallStack(ConsoleColor name, ConsoleColor file, ConsoleColor line, ConsoleColor col, CallStack stack)
        {
            Console.WriteLine("Stack trace:");
            foreach (CallFrame frame in stack.getCallFrames())
            {
                Console.Write("  in ");
                write(name, frame.name + "(" + frame.getargs().interleave(" ").concat() + ")");
                Location loc = frame.loc;
                if (loc != null)
                {
                    Console.Write(" at ");
                    if (loc.hasValues)
                        writeLoc(file, line, col, loc);
                    else
                        write(file, "(external source)");
                }
                Console.WriteLine();
            }
            Console.Write("  in ");
            write(name, "main");
            Console.Write(" at ");
            Location progLoc = stack.getProgLoc();
            if (progLoc.hasValues)
                writeLoc(file, line, col, progLoc);
            else
                write(file, "(external source)");
            Console.WriteLine();
        }
        static void writeLoc(ConsoleColor file, ConsoleColor line, ConsoleColor col, Location loc)
        {
            write(file, loc.file);
            Console.Write(", ");
            write(line, "line " + (loc.line + 1));
            Console.Write(", ");
            write(col, "col " + (loc.col + 1));
        }

        static void write(ConsoleColor color, string text)
        {
            ConsoleColor foreground = Console.ForegroundColor;
            Console.ForegroundColor = color;
            Console.Write(text);
            Console.ForegroundColor = foreground;
        }
        static void writeln(ConsoleColor color, string text)
        {
            write(color, text + Environment.NewLine);
        }
    }

    class Builtins
    {
        Interpreter interpreter;
        Parser parser;

        public Builtins(Interpreter interpreter)
        {
            this.interpreter = interpreter;
        }
        public void setParser(Parser parser)
        {
            this.parser = parser;
        }

        public void macro(Quote parse, Quote parser)
        {
            this.parser.addConcatParsingWord(interpreter, parse, parser);
        }

        #region essentials
        // runs a code file in the current context and
        // saves all definitions that were made
        public void require(string file)
        {
            interpreter.execFile(file + ".cct");
        }
        //public Exception include(string file)
        //{
        //    file += ".cct";
        //    try
        //    {
        //        interpreter.execFile(file);
        //        return null;
        //    }
        //    catch (Exception e)
        //    {
        //        return e;
        //    }
        //}
        //[BuiltinIgnore]
        //public Exception include(Symbol name)
        //{
        //    return include(name.sym);
        //}

        // print an object to the console
        public void print(object obj)
        {
            Console.Write(obj);
        }

        // appends the text to a file
        [BuiltinRename("print-file")]
        public void print_file(string text, string file)
        {
            File.AppendAllText(file, text);
        }

        // read a line from the console
        public string read()
        {
            return Console.ReadLine();
        }
        #endregion

        #region basic operations
        // calls + on two dynamic objects
        [BuiltinRename("+")]
        public dynamic _plus_(dynamic left, dynamic right)
        {
            return left + right;
        }

        // calls - on two dynamic objects
        [BuiltinRename("-")]
        public dynamic _minus_(dynamic left, dynamic right)
        {
            return left - right;
        }

        // calls * on two dynamic objects
        [BuiltinRename("*")]
        public dynamic _times_(dynamic left, dynamic right)
        {
            return left * right;
        }

        // calls / on two dynamic objects
        [BuiltinRename("/")]
        public dynamic _div_(dynamic left, dynamic right)
        {
            return left / right;
        }

        // calls > on two dynamic objects
        [BuiltinRename(">")]
        public bool _greater_(dynamic left, dynamic right)
        {
            return left > right;
        }

        // calls >= on two dynamic objects
        [BuiltinRename(">=")]
        public bool _greater_eq_(dynamic left, dynamic right)
        {
            return left >= right;
        }

        // calls < on two dynamic objects
        [BuiltinRename("<")]
        public bool _less_(dynamic left, dynamic right)
        {
            return left < right;
        }

        // calls <= on two dynamic objects
        [BuiltinRename("<=")]
        public bool _less_eq_(dynamic left, dynamic right)
        {
            return left <= right;
        }

        // calls == on two dynamic objects
        public bool eq(dynamic left, dynamic right)
        {
            try { return left == right; }
            catch { return false; }
        }

        // calls != on two dynamic objects
        public bool neq(dynamic left, dynamic right)
        {
            try { return left != right; }
            catch { return false; }
        }

        // is the object null?
        [BuiltinRename("nil?")]
        public bool is_null(object obj)
        {
            return obj == null;
        }

        // true->false, false->true
        public bool not(bool b)
        {
            return !b;
        }
        #endregion

        #region combinators (and other builtins that take quotes)
        // calls a quotation
        public void apply(Quote quote)
        {
            interpreter.exec(quote, true, false);
        }

        // sets a dynamically scoped word
        [BuiltinRename("let!")]
        public void let_bang(Quote quote, Symbol name)
        {
            interpreter.setDynamicWord(name.sym, new Word(quote.loc, quote.formals, quote.terms, 
                new HashSet<string>() { name.sym }));
        }

        // calls _true or _false depending on the condition boolean
        [BuiltinRename("if")]
        public void _if_(bool cond, Quote _true, Quote _false)
        {
            apply(cond ? _true : _false);
        }

        #region comments
        //[BuiltinRename("while")]
        //public void _while_(Quote cond, Quote body)
        //{
        //    while ((bool)ensure(() => apply(cond), typeof(bool))[0])
        //        ensure(() => apply(body));
        //}
        #endregion

        // calls the quote for each object in the enumerable
        [BuiltinRename("for-each")]
        public void for_each(IEnumerable<object> items, Quote body)
        {
            foreach (object item in items)
            {
                interpreter.push(item);
                interpreter.exec(body).waitEvent(() => interpreter.forceRunCalls());
            }
        }

        // x -> [x]
        public Quote quote(object obj)
        {
            // obj quote -> [obj] == obj [(temp-var) [temp-var]] apply
            CallStack cs = interpreter.getCallStack();
            return new Quote(cs.getLoc(), cs, new string[0], new ObjTerm(obj, cs.getLoc()));
        }
        // [a] [b] -> [a b]
        public Quote compose(Quote a, Quote b)
        {
            // [a] [b] compose -> [a [b] apply]
            // unless quote b does not have any formal parameters, in which case
            // [a] [b] compose -> [a b]
            List<Term> terms = new List<Term>(a.terms);
            if (b.formals.Length != 0)
            {
                terms.Add(new QuoteTerm(b.loc, b.formals, b.terms));
                terms.Add(new IdentTerm("apply", Location.External));
            }
            else
                terms.AddRange(b.terms);
            return new Quote(a.loc, interpreter.getCallStack(), a.formals, terms.ToArray());
        }

        // handles an exception
        public void handle(Quote dangerous, Quote handler)
        {
            interpreter.exec(dangerous, true, false, handler);
        }
        // raises an exception
        public void raise(object exc)
        {
            throw new DataException(exc);
        }
        #endregion

        #region stack effects
        // b a -> a b
        [BuiltinMultiReturn]
        public object[] swap(object b, object a)
        {
            return new object[2] { a, b };
        }

        // a -> a a
        [BuiltinMultiReturn]
        public object[] dup(object obj)
        {
            return new object[2] { obj, obj };
        }

        // a -> 
        public void drop(object obj)
        {
        }

        // c a b -> a b c
        [BuiltinMultiReturn]
        public object[] up(object c, object a, object b)
        {
            return new object[3] { a, b, c };
        }
        // b c a -> a b c
        [BuiltinMultiReturn]
        public object[] down(object b, object c, object a)
        {
            return new object[3] { a, b, c };
        }
        #endregion

        // str -> int
        [BuiltinRename("parse-int")]
        public long parse_int(string str)
        {
            return long.Parse(str);
        }

        // str -> sym
        [BuiltinRename("to-sym")]
        public Symbol to_sym(string str)
        {
            return new Symbol(str);
        }

        #region list stuff
        public List<object> list(Quote quote)
        {
            int stackSize = interpreter.stackSize();
            interpreter.exec(quote).waitEvent(() => interpreter.forceRunCalls());
            int newStackSize = interpreter.stackSize();
            int count = newStackSize - stackSize;
            if (count < 0)
                return new List<object>();
            object[] items = new object[count];
            while (--count >= 0)
                items[count] = interpreter.pop();
            return new List<object>(items);
        }
        // -> nil
        public List<object> nil()
        {
            return new List<object>();
        }
        // head tail -> list
        public List<object> cons(List<object> tail, object head)
        {
            List<object> list = new List<object>(tail);
            list.Insert(0, head);
            return list;
        }
        // head tail -> list
        public List<object> append(List<object> head, object tail)
        {
            List<object> list = new List<object>(head);
            list.Add(tail);
            return list;
        }
        // list -> bool
        [BuiltinRename("empty?")]
        public bool empty_q(List<object> list)
        {
            return list.Count == 0;
        }
        // list -> object
        public object head(List<object> list)
        {
            if (list.Count == 0)
                throw new IndexOutOfRangeException("Cannot get head of empty list.");
            return list[0];
        }
        // list -> list
        public IEnumerable<object> tail(List<object> list)
        {
            if (list.Count == 0)
                throw new IndexOutOfRangeException("Cannot get tail of empty list.");
            return list.GetRange(1, list.Count - 1);
        }
        // list -> list
        public IEnumerable<object> reverse(List<object> list)
        {
            List<object> reversed = new List<object>(list);
            reversed.Reverse();
            return reversed;
        }
        // list -> array
        [BuiltinRename("to-array")]
        public List<object> to_list(IEnumerable<object> list)
        {
            return new List<object>(list);
        }
        #endregion

        #region reflection
        public object send(object obj, IEnumerable<object> args, Symbol message)
        {
            // autoconvert longs to ints
            Type[] types = args.map(arg => arg.GetType()).map(type => type == typeof(long) ? typeof(int) : type).ToArray();
            System.Reflection.MethodInfo method = obj.GetType().GetMethod(message.sym, types);
            object[] actualArgs = args.map(arg => arg.GetType() == typeof(long) ? (int)((long)arg) : arg).ToArray();
            return method.Invoke(obj, actualArgs);
        }
        [BuiltinRename("get-prop")]
        public object get_prop(object obj, Symbol prop)
        {
            return obj.GetType().GetProperty(prop.sym).GetGetMethod().Invoke(obj, new object[0]);
        }
        [BuiltinRename("set-prop")]
        public void set_prop(object obj, object val, Symbol prop)
        {
            obj.GetType().GetProperty(prop.sym).GetSetMethod().Invoke(obj, new object[1] { val });
        }
        [BuiltinRename("get-field")]
        public object get_field(object obj, Symbol prop)
        {
            return obj.GetType().GetField(prop.sym).GetValue(obj);
        }
        [BuiltinRename("set-field")]
        public void set_field(object obj, object val, Symbol prop)
        {
            obj.GetType().GetField(prop.sym).SetValue(obj, val);
        }
        [BuiltinRename("get-indexer")]
        public object get_indexer(object obj, IEnumerable<object> args, Symbol indexer)
        {
            return obj.GetType().GetProperty(indexer.sym).GetGetMethod().Invoke(obj, args.ToArray());
        }
        [BuiltinRename("set-indexer")]
        public object set_indexer(object obj, IEnumerable<object> args, Symbol indexer)
        {
            return obj.GetType().GetProperty(indexer.sym).GetSetMethod().Invoke(obj, args.ToArray());
        }

        [BuiltinRename("get-slot")]
        public object get_slot(object obj, Symbol prop)
        {
            if (obj is StructObject)
            {
                StructObject so = (StructObject)obj;
                return so.slots[prop.sym];
            }
            return obj.GetType().GetField(prop.sym).GetValue(obj);
        }
        [BuiltinRename("set-slot")]
        public object set_slot(object obj, object val, Symbol prop)
        {
            if (obj is StructObject)
            {
                StructObject so = (StructObject)obj;
                so.slots[prop.sym] = val;
                return so;
            }
            else
            {
                obj.GetType().GetField(prop.sym).SetValue(obj, val);
                return obj;
            }
        }

        // obj -> obj
        public object copy(object obj)
        {
            if (obj is StructObject)
                return ((StructObject)obj).copy();
            else return obj;
        }
        #endregion

        [BuiltinRename("call-stack")]
        public CallStack get_call_stack()
        {
            return interpreter.getCallStack().copy();
        }

        #region helpers
        //// synchronously waits for an IHasEvent's event to fire
        //void waitEvent(IHasEvent ihe, Action beforeWait)
        //{
        //    ManualResetEvent mre = new ManualResetEvent(false);
        //    ihe.addCallback(() => mre.Set());
        //    beforeWait();
        //    mre.WaitOne();
        //}

        //// performs an action, and ensures that the interpreter
        //// will have pushed only the specified types to the datastack.
        //object[] ensure(Action action, params Type[] types)
        //{
        //    return interpreter.ensureStack(types, action);
        //}
        //object[] ensureApply(Quote quote, params Type[] types)
        //{
        //    return ensure(() => apply(quote), types);
        //}

        //// performs an action, and ensures that the interpreter
        //// will have consumed only a specified number of items from
        //// the datastack
        //void ensure(Action action, int consumption)
        //{
        //    interpreter.ensureStack(consumption, action);
        //}
        //void ensureApply(Quote quote, int consumption)
        //{
        //    ensure(() => apply(quote), consumption);
        //}
        #endregion
    }
    interface IHasEvent
    {
        void addCallback(Action action);
    }
}
